/*
 * metro-jack
 * a metronome using JACK.
 * based on code from jack-tuner (http://code.google.com/p/jack-tuner)
 * 
 * Copyright(c) 2009 S Jagannathan
 * e-mail: jagernot@gmail.com
 * Distributed under the GPL 2.0 license.
 * 
*/

#include <jack/jack.h> // for jack

// for parsing commands
#include <sstream> 
#include <stdlib.h>

#include <iostream> // for printing to and reading from console

#include <fstream> // for reading files
#include <map> // for managing tuning

#include <math.h> // we use pow (..) 

using namespace std;

typedef jack_default_audio_sample_t sample_t; // this is just float in jack/types.h

jack_client_t *client; // this is us, our id to the jack server
const char* client_name = "metro-jack"; // name of our client
jack_port_t *output; // our only port - an output port as we will only output audio
// no input port as we dont capture audio


//
// sound parameters
//

sample_t sample_rate; // samples per second. a sample is a number from -1 to 1. we must send sample_rate numbers per second to make sound.
jack_nframes_t audio_buffer_size; // audio buffer size

const int A440 = 440; // concert A
sample_t tone = A440; // in Hz, we play A440 by default
sample_t volume = 0.03; // 0 to 1. 0 = silence, 1 = loudest, choose a tolerable value

int octave = 0; // octave of a note.  A440 lies in octave 0. valid values are ..., -1, -2, 0, 1, 2, ...

sample_t delta_tone; // we apply to tone. useful for microtonal tuning

map <string, sample_t> get_tone; // map note name to note value in Hz. eg, C -> 261.626
map <string, sample_t> get_interval; // map interval number to ratio. eg, 5 -> 1.5, 4 -> 1.33; used for transposition

int bpm = 60; // default bpm

struct callback { // a callback interface
	virtual void run () = 0; // pure virtualfunction, so cant create callback objects
};

struct metro_t : callback { // metronome
	
	int wav [2]; // a square waveform
	int w; // index of square waveform
	int out; // output value
	
	metro_t () {
		
		wav[0] = 1;
		wav[1] = 0;
		w = 0;
		
		out = 1;
		
	}
	
	~metro_t () {}
	
	void run () { // implements the callback function
	
		// will be called from a ticker that runs at a certain bpm
	
		out = wav[w];
		w = !w;

	}
	
} metro;

struct ticker_t {
		
		// state of the ticker
		int state;
		enum {stopped, running};
				
		void set_bpm (float b); // set ticker bpm
		
		float beats_per_buffer; // determined by bpm; buffer is the audio buffer
		float beat; // absolute value of beat
		float beat_prev; // previous beat
		callback* callb;
		float callback_rate; // in beats ie call the callback function every callback_rate beats.
		void eval (); // evaluate the ticker and see if we need to call the callback function
		
		
		ticker_t (); 
		void set (float b, callback* cb, float cbr); // set the ticker params
		void start (); // start the ticker
		void stop (); // stop the ticker
		void set_callback (callback* cb, float cbr = 1); // set callback and callback rate
		
		// called when sample_rate or audio_buffer_size changes
		static float secs_per_buffer;
		static void calc_secs_per_buffer ();
		
};

ticker_t::ticker_t () {
	
	stop ();
	beats_per_buffer = 0;
	callb = 0;
	callback_rate = 0;
	
}

void ticker_t::set (float b, callback* cb, float cbr) {
	
	set_bpm (b);
	set_callback (cb, cbr);
	
}

void ticker_t::set_bpm (float b) {
	
	float bps = b / 30; 
	
	beats_per_buffer = secs_per_buffer * bps;
		
}

void ticker_t::start () {
	
	beat = beat_prev = 0;
	state = running;
	
}

void ticker_t::stop () {
	
	state = stopped;
	beat = beat_prev = -1;
	
}

void ticker_t::set_callback (callback* cb, float cbr) {
	
	callb = cb;
	callback_rate = cbr;
	
}

void ticker_t::eval () {
		
	beat += beats_per_buffer;
	
	float delta_beat = beat - beat_prev - callback_rate;
	if (delta_beat >= 0) {
		callb->run ();
		beat_prev = beat + delta_beat;
	}
		
}

void ticker_t::calc_secs_per_buffer () {
	
	secs_per_buffer = audio_buffer_size * 1. / sample_rate;
	
}

float ticker_t::secs_per_buffer = 0;

ticker_t tik; // ticker

bool read_map (map<string, sample_t>& m, const string& f) {
	
	//
	// read map from file
	//
	
	ifstream file (f.c_str(), ios::in);
	
	if (!file) {
		cout << "bad file: " << f << endl;
		return false;
	} {
				
		m.clear (); // clear old map
		
		string ignore;
		int n;
		file >> ignore >> n;
		
		for (int i = 0; i < n; ++i) {
			string sharp, flat;
			sample_t value;
			file >> sharp >> flat >> value;
			m [flat] = value;
			m [sharp] = value;
		}
		
		return true;
		
	}
	
}


/*
 * square waveform parameters
 *  
 * we use a square waveform to sound out the tone.
 * what, no sine waveform? yes. why? different waveforms 
 * only affect the timbre of the sound not the pitch so 
 * we can use anything. and finally, the square waveform 
 * is simple to code. easy to understand for beginners to 
 * sound synthesis.
*/

int pulse_width;
int current_width;
int current_height;
float current_error, initial_error;

void prep_square_waveform () {
	
	float f_pulse_width = sample_rate / (2 * tone);
	
	// handle reminder
	
	pulse_width = (int) f_pulse_width;
	current_width = pulse_width;
	
	initial_error = f_pulse_width - pulse_width;
	current_error = initial_error;
	
	current_height = 0;
	
}

int process (jack_nframes_t nsamples, void *arg) {

	// jack will call this function when it wants audio from us
	// ie this is the function where we generate the square waveform to make sound.

	sample_t* sample_buffer = (sample_t *) jack_port_get_buffer (output, nsamples); // sample buffer
	
	sample_t current_volume [] = {volume, -volume};
	
	if (tik.state == ticker_t::running) tik.eval ();
	
	for (int i = 0; i < nsamples; ++i) {
		
		if (current_width-- == 0) { // half a cycle of the square waveform is complete
	
			current_height = !current_height; // switch to other half
			
			// handle error due to float division in prep_square_waveform ()
			if (current_error >= 1) {
				current_error -= 1;
				current_width = pulse_width + 1;
			} else {
				current_width = pulse_width;
			}
			
			current_error += initial_error;
			
		}
		
		sample_buffer[i] = metro.out * current_volume [current_height];
		
	}
	
	return 0;
	
}

int srate (jack_nframes_t nframes, void *arg) { // jack will call this function when the sample rate changes

	sample_rate = jack_get_sample_rate (client);
	ticker_t::calc_secs_per_buffer ();
	tik.set_bpm (bpm);
	prep_square_waveform ();
	return 0;
	
}

int audio_buffer_size_changed (jack_nframes_t, void* arg) {
	
	audio_buffer_size = jack_get_buffer_size (client);
	ticker_t::calc_secs_per_buffer ();
	tik.set_bpm (bpm);
	
}

int check_and_set_tone () {
	
	const sample_t min_tone = 20;
	sample_t max_tone = sample_rate / 2; // min frequency and max frequency 
	if (tone < min_tone) tone = min_tone; else if (tone > max_tone) tone = max_tone;
	
	static char buf [32] = {0}; sprintf (buf, "%f", tone);
	cout << "tone is " << buf << " Hz." << endl;
	prep_square_waveform (); // need to recalculate the square waveform parameters as tone has changed
	
}

int main () { // program starts running here
	
	jack_status_t status; 
	jack_options_t options = JackNullOption;
	
	// try to connect to the jack server. we assume jack server is running on this machine.
	client = jack_client_open (client_name, options, &status, 0);
	if (client == 0) {
		cout << "couldnt connect to jack server." << endl;
		return 1;
	}
	
	// get sample rate for our client
	sample_rate = jack_get_sample_rate (client);

	// jack will call srate (..) when sample rate has changed.
	jack_set_sample_rate_callback (client, srate, 0);

	// get audio buffer size for our client
	audio_buffer_size = jack_get_buffer_size (client);
	
	// jack will call audio_buffer_size_changed (..) when audio buffer size has changed
	jack_set_buffer_size_callback (client, audio_buffer_size_changed, 0);
	
	// setup timer for metronome
	ticker_t::calc_secs_per_buffer ();
	tik.set (bpm, &metro, 1);
	tik.start ();
	
	// jack will call process(..) when it wants us to give audio.
	jack_set_process_callback (client, process, 0);
			
	// setup the output port where we will output audio
	const char* port_name = "out";
	output = jack_port_register (client, port_name, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
	if (output == 0) {
		cout << "couldnt create output port" << endl;
		return 1;
	}
	
	// read notes & their values from file ie C = 261.626 Hz, C#, D, .....Bb and B
	if (read_map (get_tone, "notes") == false) return 1;
	
	// read default interval tuning from file. et is equal temperament
	if (read_map (get_interval, "et") == false) return 1;
		
	// all done, greet user and get ready to recieve commands
	
	string commands = "Commands available: help (h), tone (t), delta (d), octave (o), up (>), down (<), intervals_file (f), volume (v), bpm (b), quit (q)";
	
	cout << endl << "Welcome to " << client_name << endl;
	cout << "Copyright (c) 2009 S. Jagannathan" << endl;
	cout << "E-mail comments & suggestions to: jagernot@gmail.com" << endl;
	cout << "Distributed under GPL 2.0 license" << endl << endl;
	cout <<  commands << endl;
	cout << endl;
	cout << "sample_rate is " << sample_rate << " numbers per second" << endl;
	check_and_set_tone ();
	cout << "volume is " << volume << endl << endl;

	// tell jack we are ready to send audio. after we activate ourselves, jack will call process (..) whenever it wants audio from us.
	if (jack_activate (client) != 0) {
		cout << "couldnt get jack to accept audio" << endl;
		return 1;
	}

	bool read_tone = false, read_volume = false, read_octave = false;
	bool read_delta = false, read_up = false, read_down = false;
	bool read_file = false;
	bool read_bpm = false;
		
	string word;
	while (cin >> word) {
				
		if (read_tone) { // read tone in Hz.

			read_tone = false;
			if (word.length() > 0) word[0] = toupper (word[0]); // support lower case note names
			tone = get_tone [word];
			if (tone == 0) { // word is not a note name but a tone value
				tone = atof(word.c_str ());
			}
			check_and_set_tone (); 

		} else if (read_volume) { // read volume value
		
			read_volume = false;
			volume = atof (word.c_str());
			if (volume < 0) volume = 0; else if (volume > 1) volume = 1; 
			cout << "volume is " << volume << endl;
			
			// no need to recalculate square wave here as latest volume will be dynamically used
			
		} else if (read_octave) { // read octave number
			
			read_octave = false;
			octave = atoi (word.c_str());
			tone *= pow (2, octave);
			check_and_set_tone ();
			
		} else if (read_delta) { // read delta_tone and apply to tone
			
			read_delta = false;
			delta_tone = atof (word.c_str());
			tone += delta_tone;
			check_and_set_tone ();
			
		} else if (read_up) { // read interval to transpose the tone up
			
			read_up = false;
			sample_t interval = get_interval [word];
			if (interval == 0) {
				cout << "invalid transposition" << endl;
			} else {
				tone *= interval;
				check_and_set_tone ();
			}
			
		}	else if (read_down) { // read interval to transpose the tone down
			
			read_down = false;
			sample_t interval = get_interval [word];
			if (interval == 0) {
				cout << "invalid transposition" << endl;
			} else {
				tone /= interval;
				check_and_set_tone ();
			}
			
		} else if (read_file) { // read intervals file
			
			read_file = false;
			if (read_map (get_interval, word)) cout << "tuning using: " << word << endl;
			
		} else if (read_bpm) { // read bpm of metronome
			read_bpm = false;
			bpm = atof (word.c_str());
			tik.set_bpm (bpm);
		} 
		else if (word == "quit" || word == "q") break; // quit tuner
		else if (word == "tone" || word == "t") { // next word is the tone value
			read_tone = true;
		} else if (word == "volume" || word == "v") { // next word is the volume value
			read_volume = true;
		} else if (word == "octave" || word == "o") { // next word is octave shift for tone
			read_octave = true;
		} else if (word == "delta" || word == "d") { // next word is delta tone
			read_delta = true;
		} else if (word == "up" || word == ">") { // next word is transpose up
			read_up = true;
		} else if (word == "down" || word == "<") { // next word is transpose down
			read_down = true;
		} else if (word == "intervals_file" || word == "f") { // next word is intervals file
			read_file = true;
		} else if (word == "help" || word == "h") {
			cout << commands << endl;
		} else if (word == "bpm" || word == "b") {
			read_bpm = true;
		}else {
			cout << word << " not found." << endl;
		}
		
	}
	
	jack_client_close (client);
	
	return 0;
	
}



